#include "loader.h"

#include <QDebug>
#include <QXmlStreamReader>
#include <QFileInfo>
#include <QDir>
#include "window.h"
#include "script.h"

Loader::Loader(QObject *parent) : QObject(parent)
{
}

Loader &Loader::globalLoader()
{
    static Loader loader;
    return loader;
}

Element *Loader::loadXML(QFile &file)
{
    if(hasCached(file)){
        return getCacheCopy(file);
    }

    if(!file.open(QFile::ReadOnly))
        return NULL;

    QXmlStreamReader reader(&file);
    return loadXML(reader, QFileInfo(file).absoluteFilePath());
}

Element *Loader::loadXML(const QString &path)
{
    QFile file(path);
    return loadXML(file);
}

Element *Loader::loadXML(QXmlStreamReader &root, const QString &path)
{
    if(!root.readNextStartElement()){
        qDebug() << Q_FUNC_INFO << root.errorString();
        return NULL;
    }

    Context context;

    if(root.name()=="Page"){
        root.readNextStartElement();
        if(loadImport(root, &context, path)&& !root.readNextStartElement()){
            qDebug() << Q_FUNC_INFO << root.errorString();
            return NULL;
        }
    }else if(root.name()=="Index"){
        loadIndex(root, &context, path);
        return NULL;
    }

    Element *ret = NULL;

    Script::getInstance()->engine()->pushContext();

    // 添加系统环境
    QString system_env =
    "var switchWindowTo = (function(from, to){ "
    "   var w=from; var id = to; "
    "   return (function(){ "
    "       w.switchWindowTo(id); "
    "   });"
    "});";

    Script::getInstance()->engine()->evaluate(system_env);

    if(root.name()=="Window")
        ret = loadWindow(root, &context, path);
    else
        ret = loadElement(root, &context, path);
    Script::getInstance()->engine()->popContext();

    if(ret){
        ret->setPath(path);
        if(!path.isEmpty())
            cache(path, *ret);
    }
    return ret;
}

Window *Loader::loadXMLFromIndex(const QString &name)
{
    if(!index_name_path_.contains(name)){
        return NULL;
    }

    return qobject_cast<Window*>(loadXML(index_name_path_[name]));
}

bool Loader::loadImport(QXmlStreamReader &reader, Context *context, const QString &path)
{
    if(reader.name()!="Import")
        return false;

    while(!reader.atEnd()&&!(reader.tokenType()==reader.EndElement&&reader.name()=="Import")){
        if(reader.readNext() != reader.StartElement) continue;
        if(reader.name() == "Element"){
            QXmlStreamAttributes attrs = reader.attributes();
            QString src = QFileInfo(path).dir().absoluteFilePath(attrs.value("src").toString());
            QString name = attrs.value("type").toString();
            Element *e = loadXML(src);
            if(e && !name.isEmpty()){
                context->element_types.insert(name, QSharedPointer<Element>(e->clone()));
            }else{
                qWarning() << Q_FUNC_INFO << QString("Import Error: file:%1 src:%2, name:%3").arg(path).arg(src).arg(name);
            }
            delete e;
        }else if(reader.name()=="Include"){
            loadInclude(reader, context, path);
        }else{
           reader.skipCurrentElement();
        }
    }
    return true;
}

bool Loader::loadInclude(QXmlStreamReader &root, Context *context, const QString &path)
{
    if(root.name()!="Include"){
        qWarning() << "Error Tag for include" << root.name();
        return false;
    }
    QString src = root.attributes().value("src").toString();
    if(src.isEmpty()){
        qWarning() << QString("Need src in <Include>: path:%1").arg(path);
        return false;
    }

    QFile file(QFileInfo(path).dir().absoluteFilePath(src));
    if(!file.open(QFile::ReadOnly)){
        qWarning() << "Can't open file"<< QFileInfo(file).absoluteFilePath();
        return false;
    }

    QXmlStreamReader reader(&file);
    reader.readNextStartElement();
    return loadImport(reader, context, QFileInfo(file).absoluteFilePath());
}

Element *Loader::loadWindow(QXmlStreamReader &reader, Context *context, const QString &path)
{
    if(reader.name()!="Window")
        return NULL;

    Window *w = new Window();
    if(!w->loadXML(reader, context, Element::StayAfterLoaded)){
        qDebug() << Q_FUNC_INFO << QString("Load Window failed: file:%1 name:%2").arg(path).arg(w->name());
        delete w;
        w = NULL;
        return NULL;
    }

    while(!reader.atEnd()){
        if(!reader.readNextStartElement() && reader.hasError()){
            qWarning() << reader.errorString();
            continue;
        }if(reader.name()=="Code"){
            loadCode(reader, context, path);
        }else{
            Element *e = loadElement(reader, context, path);
            if(!w->addElement(e)){
                qDebug() << Q_FUNC_INFO << QString("Add %1 to Window failed: file:%2").arg(reader.name().toString()).arg(path);
            }
        }
    }

    return w;
}

bool Loader::loadCode(QXmlStreamReader &reader, Context *context, const QString &path)
{
    if(reader.name()!="Code"){
        qWarning() << "Unknow Code Tag:" << reader.name();
        return false;
    }

    QString program = reader.readElementText();
    QScriptEngine *engine = Script::getInstance()->engine();
    engine->evaluate(program);
    if(engine->hasUncaughtException()){
        qWarning() << engine->uncaughtException().toString() << program;
        engine->clearExceptions();
        return false;
    }else{
        return true;
    }
}

bool Loader::loadIndex(QXmlStreamReader &reader, Context *context, const QString &path)
{
    if(reader.name()!="Index"){
        qWarning() << QString("Error tag <%1> for Index").arg(reader.name().toString());
        return false;
    }

    QDir root = QFileInfo(path).absoluteDir();
    while(!reader.atEnd()){
        reader.readNextStartElement();
        if(reader.tokenType()!=reader.StartElement) continue;
        if(reader.name()=="Page"){
            QXmlStreamAttributes attrs = reader.attributes();
            QString name = attrs.value("name").toString();

            bool ok;
            int id = attrs.value("id").toInt(&ok);
            if(!ok) id = -1;

            QString src = attrs.value("src").toString();
            if(src.isEmpty()){
                qWarning() << Q_FUNC_INFO << QString("Empty path in index for (%1, %2)").arg(name).arg(id);
                continue;
            }else{
                QString p = root.absoluteFilePath(src);
                if(!name.isEmpty()){
                    if(index_name_path_.contains(name)){
                        qWarning() << Q_FUNC_INFO << QString("Duplicate name %1 at index.").arg(name);
                        continue;
                    }else{
                        index_name_path_.insert(name, p);
                    }
                }else{
                    qWarning() << Q_FUNC_INFO << QString("Empty name for %1").arg(src);
                }
                if(id!=-1){
                    if(index_id_path_.contains(id)){
                        qWarning() << Q_FUNC_INFO << QString("Duplicate id %1 at index.").arg(id);
                        continue;
                    }else{
                        index_id_path_.insert(id, p);
                    }

                }
            }
        }else{
            qWarning() << Q_FUNC_INFO << QString("Unknow tag %1 at %2").arg(reader.name().toString()).arg(path);
            reader.skipCurrentElement();
        }
    }
    return true;
}

Element *Loader::loadElement(QXmlStreamReader &reader, Context *context, const QString &path)
{
    Element *e = createElement(reader.name().toString(), context);
    if(!e){
        qWarning() << Q_FUNC_INFO << QString("Can't recognize Element type:%1 file:%2").arg(reader.name().toString()).arg(path);
        reader.skipCurrentElement();
    }else{
        if(!e->loadXML(reader, context)){
            delete e;
            e = NULL;
            qWarning() << Q_FUNC_INFO << QString("Load %1 failed. file:%2").arg(reader.name().toString()).arg(path);
        }
    }

    return e;
}

Element *Loader::createElement(const QString &type, const Context *context) const
{
    if(context->element_types.contains(type)){
        return context->element_types[type]->clone();
    }

    // search in build-in context
    if(Context::getGlobalContext().element_types.contains(type)){
        return Context::getGlobalContext().element_types[type]->clone();
    }

    return NULL;

}

bool Loader::hasCached(const QString &path) const
{
    return element_cache_.contains(path);
}

bool Loader::hasCached(QFile &file) const
{
    return hasCached(QFileInfo(file).absoluteFilePath());
}

Element *Loader::getCacheCopy(QFile &file)
{
    QString path = QFileInfo(file).absoluteFilePath();
    return getCacheCopy(path);
}

Element *Loader::getCacheCopy(const QString &path)
{
    Element *ret = NULL;
    if(element_cache_.contains(path)){
        ret = element_cache_[path]->clone();
    }
    return ret;
}

void Loader::cache(const QString &path, const Element &e)
{
    QFileInfo info(path);
    element_cache_.insert(info.absoluteFilePath(), e.clone());
}

void Loader::cache(QFile &file, const Element &e)
{
    cache(QFileInfo(file).absoluteFilePath(), e);
}
